classdef (CaseInsensitiveProperties = true) plan < handle
    properties
        root = []; % Nodo desde el que planear
		goal = []; % Nodo al que hay que llegar (necesita ser la representacion del nodo, no el objeto node)
        movesLimit = []; % Cantindad maxima de movidas que se pueden hacer antes de perder la partida
		trans = []; % Matriz de transiciones entre nodos
		learningConstants = [];
		
		heuristic = []; % Heuristica para el plan
		depth = []; % Profundidad de planeamiento
		time = []; % Tiempo permitido para planear
		explore = []; % Parametro de exploracion
        iterations = []; % Cantidad de iteraciones de planeamiento
        
        learnValues = true; % Si se actualizan los valores de los nodos con la experiencia o no
    end
    properties (SetAccess = protected)
		plans = {}; % Serie de movidas que devuelve el plan
		nodes = {}; % Lista de objetos node que fueron explorados
        plansId = []; % Identidad de cada plan una vez que se aplica uniquePlan
		
		endFlag = 0; % Referencia interna para saber si se alcanzo el objetivo en el plan
        leafFlag = 0;
        original_root = [];
        original_movesLimit = [];
    end
    
    methods
        % Creador
        function self = plan(root,goal,ml,trans,lc,h,d,t,e,i)
            self = self.setGame(root,goal,ml,trans,lc);
            self = self.setParameters(h,d,t,e,i);
        end
        function self = setParameters(self,h,d,t,e,i)
            self.heuristic = h;
            self.depth = d;
            self.time = t;
            self.explore = e;
            self.iterations = i;
        end
        function self = setGame(self,root,goal,ml,trans,lc)
            if ~isa(root,'node')
                error('Root node must be of class node');
            else
                if root.index~=1
                    warning('MATLAB:Root1','Root''s index must be equal to one.\nThe planning class will change its value to 1.');
                    root.index=1;
                end
                if root.parent~=0 && root.parent~=1
                    warning('MATLAB:Root2','Roor''s parent must be equal to 0 or 1.\nThe planning class will change its value to 1.');
                    root.parent=1;
                end
                if isempty(root.value)
                    warning('MATLAB:Root3','Root''s value has not been initialised.\nIts starting value will be set to inf.');
                    root.value = inf;
                end
%             elseif isempty(root.children)
%                 warning('Root node has no children');
            end
            self.root = root; % Debe ser una estructura nodo
            self.original_root = root;
            self.goal = goal;
            self.movesLimit = ml;
            self.original_movesLimit = ml;
            self.trans = trans;
            self.learningConstants = lc;
            self.nodes = {self.root};
        end
        function continuePlaying(self,root,done_moves)
            % When the planner executes a plan that doesnt finish, it must
            % start to plan again from another root with a new movesLimit
            self.root = root;
            self.movesLimit = self.movesLimit-done_moves;
            self.plans = [];
            self.plansId = [];
            self.endFlag = 0;
            self.leafFlag = 0;
        end
        function newStart(self,varargin)
            % Esta funcion se usa para poder repetir muchas veces un juego.
            % La entrada opcional es un bool que si es true borra la
            % estructura de nodos. Si es false, no.
            self.root = self.original_root;
            self.movesLimit = self.original_movesLimit;
            self.plans = [];
            self.plansId = [];
            self.endFlag = 0;
            self.leafFlag = 0;
            if ~isempty(varargin) && isa(varargin{1},'logical') && varargin{1}
                self.nodes = {self.root};
            end
        end
        
        % Funcion principal de planeamiento
        function [BPlan,BPlanFreq] = planning(self)
            % Funcion principal de planeamiento. Una vez que estan
            % inicializados todos los parametros y los datos del juego,
            % hace una cantidad de planes (loop de iteraciones) de
            % profundidad variable. En cada plan, deciende por el arbol y
            % despues actualiza el valor de los nodos. Cuando tiene los K
            % planes, busca el mejor entre ellos.
            tic; % Reloj
            doUpdate = self.learnValues;
            for iteration = 1:self.iterations
                i = 1;
                history={self.root}; %#ok<*PROP>
                self.endFlag = 0;
                self.leafFlag = false;
%                 fprintf(1,'%1.0f\n',iteration);
                looseFlag = false;
                while not(self.leafFlag) && toc<=self.time && i<(self.depth+1) && self.endFlag == 0 && i<=(self.moveslimit+1)
                    if i == self.movesLimit+1 % Si te pasas de la maxima cantidad de movidas permitidas, perdes
                        if history{i}.representation ~= self.goal
                            looseFlag = true;
                            break
                        else
                            self.endFlag = 1;
                        end
                    end
                    % Expande los nodos hijos mientras no se le haya agotado el tiempo o haya recibido el endFlag
                    [history{i+1}]=self.descendTree(history{i});
                    i=i+1; % Ya hago el i+1 entonces todas las llamadas a este nodo agregado seran con i hasta el end del while
%                     fprintf(1,'%1.0f\n',nodes{i}.representation);
                    if self.leafFlag % Si tengo leafFlag true, el anteultimo nodo no tenia hijos y descendTree devuelve 0
                        history{end} = [];
                    end
                end
                if doUpdate
                    [history] = self.updateValue(history,looseFlag);
                end
                % No se si almacenarme en el plan la secuencia de nodos
                % visitados (que va a contener valores desactualizados) o
                % el indice de esos nodos dentro de self. Creo que si le
                % paso nodes, puede recuperar facil el indice de los nodos
                % en self.
                self.plans{end+1} = history;
            end
            [BPlan,BPlanFreq] = self.bestPlan; % puedo obtener tambien la cantidad de veces que se repite el plan
        end
        
        % Funcion que establece como se baja por el arbol
        function [outputNode] = descendTree(self,currentNode) % Aca deberia entrar el parametro plan.explore
            % Esta funcion define como se baja por las ramas del arbol de
            % exploracion. Toma el nodo de partida, expande sus hijos y
            % revisa cuales cumplen con el criterio de valor para ser
            % seleccionados
            
            depth = currentNode.treeLayer;
            if isempty(currentNode.children) % Si no estan inicializados los hijos del nodo
                currentNode.children = self.expandNode(currentNode);
                self.nodes{currentNode.index}.children = currentNode.children;
            end
            if ~isempty(currentNode.children)
                childs = currentNode.children; % Devuelve las representaciones del estado de los nodos hijos
                [childNodes,newNodes] = self.find_in_nodes(childs,currentNode,depth); % Devuelve una lista con los objetos nodos hijos que estan en self.nodes
                % Para que cuando tengo un empate devuelva un nodo al azar
                % en lugar de siempre elegir el primero, voy a hacer una
                % permutacion al azar de childNodes
                ix = randperm(length(childNodes));
                childNodes = childNodes(ix);
                newNodes = newNodes(ix);
                
                v = [childNodes.value];
                vis = [childNodes.nvisits];
                prob = self.explore(v,vis);
                prob = cumsum(prob);
                r = rand;
                argmax = find(r<=prob,1);
                outputNode = childNodes(argmax);
                outputNode = outputNode.addVisit;
                if newNodes(argmax) % Si el nodo elegido no existia en self.nodes
                    self.nodes{end+1} = outputNode;
                else
                    self.nodes{outputNode.index}.nvisits = outputNode.nvisits;
                end
                if outputNode.representation == self.goal
                    self.endFlag = 1;
                end
            else % El nodo que se busca expandir no tiene hijos
                outputNode = 0;
                self.leafFlag = true;
            end
        end
        
        function outputs = expandNode(self,expandable)
            % Toma el nodo expandable y a partir de su representacion,
            % encuentra los estados hacia los que puede ir por medio de la
            % matriz de transiciones
            state = expandable.representation;
            temp = self.trans(state);
            moves_containers = keys(temp);
            outputs = [];
            for m_ind = 1:length(moves_containers)
                m = moves_containers{m_ind};
                outputs = [outputs temp(m)];
            end
        end
        
        function [childNodes,newNodes] = find_in_nodes(self,childs,parent,depth)
            % Esta funcion toma la representacion de los nodos hijos
            % (variable childs) y busca nodos que esten en self.nodes que
            % contengan la misma representacion de su estado y de su nodo
            % padre (no usa el indice del padre). Tambien devuelve un bool
            % que indica si childNodes(i) ya existe en newNodes
            temp = self.nodes;
            if isa(temp,'cell')
                temp = [temp{:}];
            end
            reps = [temp.representation];
%             pars = [temp.parent_rep];
            layers = [temp.treeLayer];
            
            childNodes = cell(1,length(childs));
            newNodes = false(1,length(childs));
            l = length(self.nodes);
            for c_ind = 1:length(childs)
                c=childs(c_ind);
                inds = c==reps & (depth+1)==layers;% & parent_rep==pars; % Indice de self.nodes donde se encuentra el nodo con representacion c y padre parent_index
                if sum(inds)==1
                    childNodes{c_ind}=self.nodes{inds};
                    newNodes(c_ind) = false;
                elseif sum(inds)==0
                    vacio = node(c,parent.representation,parent.index,l+1,depth+1); % Despues se agrega solo un nodo a la lista nodes asique puedo poner que el indice de todos los hijos desconocidos es l+1
                    vacio.value = self.heuristic(vacio,parent); % Valor inicial del nodo
                    childNodes{c_ind} = vacio;
                    newNodes(c_ind) = true;
                else
                    error('UNKNOWN ERROR\nTwo identical nodes exist within the tree')
                end
            end
            childNodes = [childNodes{:}];
        end
        
        function [history] = updateValue(self,history,looseFlag) % Actualiza en la estructura self.nodes
            % Esta funcion toma toda la historia de nodos por los que se
            % transito en un plan y actualiza el valor de toda la tira. Es
            % una actualizacion de la onda de un aprendizaje Monte Carlo
            % (Sutton capitulo 5).
            % Hubiese querido actualizar el valor al estilo de Q-learning
            % pero me dio medio raro asi que directamente actualizo como el
            % valor del nodo siguiente por la constante gamma de
            % decaimiento
            endNode = history{end};
            if looseFlag
                endNodeValue = 0;
            else
                endNodeValue = endNode.value;
            end
            if isempty(self.nodes{end}.value) || isinf(self.nodes{end}.value)
                if length(history)==1
                    endNodeValue = self.heuristic(endNode,endNode);
                else
                    endNodeValue = self.heuristic(endNode,history{end-1});
                end
                endNode.value = endNodeValue;
                self.nodes{endNode.index} = endNode;
            end
            history{end}.value = endNode.value;
            self.nodes{endNode.index}.value = endNode.value;
            % Voy a actualizar los valores de los nodos almacenados en
            % self.nodes usando la historia de nodos recorridos en el plan
            i = length(history)-1;
            updatedList = endNode.index;
            
            if isa(history,'cell')
                history = [history{:}];
            end
            history_index = [history.index];
            next_node_val = endNodeValue;
            while i>0
                updatedFlag = false;
                if ismember(history_index(i),updatedList)
                    updatedFlag = true;
                end
                if isempty(history_index(i))
                    error('Trying to update value of a node without proper indexing');
                else
                    v = self.nodes{history_index(i)}.value;
                end
                if ~updatedFlag
                    if isempty(v) || isinf(v)
                        v = self.learningConstants.gamma * next_node_val;
                    else
                        v = v + self.learningConstants.alpha *...
                            (self.learningConstants.gamma * next_node_val - v);
                    end
                    updatedList(end+1) = history_index(i);
                    self.nodes{history_index(i)}.value = v;
                end
                history(history_index==history_index(i)).value=v;
                next_node_val = v;
                i = i-1;
            end
        end
        
        function [BPlan,maxRep] = bestPlan(self)
            % Funcion que selecciona a partir de las estructura self.plans
            % el plan que es aparentemente el mejor. Se elige el plan con
            % mas repeticiones.
            
            % Tengo que generar algo que haga un unique de plans
            [p,rep,id] = uniquePlan(self.plans);
            self.plansId = id;
            [maxRep,repInd] = max(rep);
            BPlan = p{repInd};
        end
    end
end